程式人最喜歡搞縮寫,創造一堆原則之後,最好玩的就是有時候原則之間會互相打架。
例如我記得就一堆書裡喜歡戰 Clean Code。
J葛嘛,只能說 Context 不同,我們要重其意而非重其形。
只有這樣才能這適當的時候做出 trade off,知道為何而做而非為做而做。
應該且僅有一個原因引起類別的變更。
例:
若是要開發一個帳號管理相關的功能,應該把帳號管理的屬性及行為分開。
將帳號資訊封裝成一個物件(AccountEntity),操作帳號的行為抽取成業務邏輯(AccountService)。
若要取得資訊則取得或生成AccountEntity,操作行為則實作AccountService。
如此可降低複雜度、提升可讀及可維護性、降低耦合避免連動。
註:
介面雖容易達到單一職責,但類別實作則反之,須注意SRP切割的粒度,過度使用會造成類別暴增。
建議介面一定要做到單一職責,類別設計盡量做到只有一個原因會引起變更,而方法則盡可能一次只做一件事。
對擴充開放,對修改關閉。
例:
在滿足其餘OOAD原則的前提下,此為總綱。
對於一個已經release的系統,有新增需求。
如果已滿足了SRP,會讓變動的類別減少,避免連動產生副作用。
如果已滿足了LSP,可以確實的規範繼承行為,釐清責任的分派。
如果已滿足了LKP,規範物件之間的溝通,以最低可見度為原則。
如果已滿足了ISP,可良好的規範介面的粒度,避免負責任務過度集中。
如果已滿足了DIP,物件溝通大部分透過抽象介面,只要確實遵守抽象介面的規範,針對實作類別修改即可。
所以便能更安全地擴充系統功能,避免大幅度的修改產生連鎖副作用。
子類別的實作必須可以完全替代父類別。
例:
有個Rectangle類別定義了長寬跟計算面積的函數,有個Square類別繼承它並且覆寫計算面積的函數。
結果當使用者使用多型來操作時(Rectangle r = new Square(), double area = r.calArea()),卻發生計算錯誤的狀況。
違犯這個原則會讓使用者有錯誤的預期。
註:
LSP和SRP以及ISP有很強的關聯,一旦類別負責過多責任,子類別一旦無法完整支援就會違反LSP。
要遵守LSP,則子類別必須是加強父類別的功能,不能減少。
物件之間的依賴關係應該建立在物件所需的最小interface上。
例:
設計介面時不能過度龐大也不能過度精細違反SRP。
基本原則是讓一個介面只服務一個子模組或是業務邏輯。
適度重構來減少介面方法。
註:
ISP是對介面進行規範的原則,試圖對介面的粒度進行規範。
原先以為的ISP其實是面對介面撰寫而非實現。
高階模組不應該依賴於低階模組,兩者都應該依賴於抽象。
抽象不應該相依於實作,實作應該相依於抽象。
例:
一台車子(高階)不應該依賴於特殊規格的零件(低階),兩者應該依賴公開規格(抽象)來做溝通。
公開規格(抽象)不應該相依於特殊零件(實作),特殊零件(實作)應該相依於公開規格(抽象)。
註:
實作類別內部程式碼可自行實作,但必定要符合介面訂定之規則實作,如此可實現高內聚(內部模組化)。
物件之間溝通要用公開的 介面 而不是特定 實作,降低物件之間的耦合,以滿足開閉原則。
核心思想就是說我們必須要審慎的去決定要在哪個物件中創建新的物件。
畢竟簡單的在 Class A 裡頭 new ClassB 雖然輕鬆寫意,不過這可就是直接了當的耦合。
如果累積過多物件的關聯,未來改動也是很麻煩的。
所以像是Dependency Injection或是像諸多的Creational Design Pattern都是在處理這樣的事。
這個經典到不用多說,94 MVC Pattern 的 C。
什麼你跟我說你不知道 MVC!
那我只能叫你回去叫你阿嬤來囉 (歐飛
一组高内聚的职责分配给一个虚构的或处理方便的“行为”类
把提高內聚的責任分配給一組虛擬的行為類別。
經典例子是 Design Pattern 中的:Adapter, Strategy。
就是SRP啦,哈哈。
物件的程式碼應該要有很高的比率只和物件內其他有關的程式碼有關聯,而對外部的程式碼,物件或元件等的關聯度要愈低愈好(最佳的狀態是零耦合)。
Data Coupling > Message Coupling > Zero Coupling
如果你有兩個元件,你應該怎麼避免他們直接的耦合。
詳細可參考以下 Design Pattern:Proxy, Decorator, Mediator
這個也別想難了,就是物件導向三大原則中多型的應用。
叫你不要亂寫case來搗亂程式的流向,這個的在 Design Pattern 也有很多相關的主題。
例如:Adapter, Command, Composite, Strategy, Observer
一言以蔽之,封裝變動。
如果變動部分都被封裝起來,對外部的接口都是低耦合的,那麼修改程式造成的影響也會變少。
一個物件應該對其他物件有最少了解
例:
設計物件時應謹慎設定每個屬性跟方法的可見度 (public, protected, private)。
除了減少不必要的資訊暴露給外部,也避免外部有機會改變內部狀態。
註:
LKP核心概念是對類別之間需解耦、弱耦合。只有弱耦合以後,類別的重複利用率才可以提升,
附帶的影響就是會產生大量的中轉或跳轉類別,使系統複雜性上升,因此使用時須反覆衡量。
不要寫出重複的程式碼,應考慮提煉至父類或是委託另一個物件處理。
如果有重複的變數值產生,也建議以public final宣告以增加維護性。
簡單是軟體設計的目標,簡單的程式碼佔用時間少,漏洞少,並且易於修改。
畢竟零件多機構多維修機會就高是簡而易懂的道理。
除非你需要它,否則避免建立你不要的程式碼。
邏輯跟不要重複造輪子差不多。
物件之間彼此的溝通應盡量以抽象類別或是介面來溝通而非實作類 (善用多型)。
概念類似DIP,試圖做到物件之間的解耦,未來在修改時可直接替換實作,降低修改的副作用。
繼承的優點是可以把重複的屬性及行為移至父類別,使得繼承的子類別可以直接獲得這個屬性及方法,以避免DRY。
但父類別的方法不一定永遠合適使用,在不斷的繼承衍生類別時常有覆寫父類別屬性及方法或是委託父類別來操作的情境。
這便會造成維護及使用上的麻煩。
所以在創造一個物件時,好的做法是將其每個部位都以抽象介面來組合成品。
之後在維護及修改時直接抽換抽象介面的實作類即可,以達成解耦。
例:
鳥抽象類別 (麻雀、鴨子、老鷹)。
有一個抽象方法(覓食),交給不同的子類去實作。
現在有個雁子類別,因為覓食方式跟鴨子一樣,為避免DRY所以增加雁科類別,實作覓食方法。
再讓雁子跟鴨子繼承雁科類別以獲得相同的覓食方法。
如果今天有個類似的的繼承了雁科的鷲鷹科。
他實作了飛行這個抽象方法來讓所有繼承他的子類獲得相同的飛行方法。
可是此時某些鷲鷹科子類別有不同的覓食方法,必須做覆寫。
最後就會越來越龐大,難以維護及使用。
好的方法應該是使用組合,設計一個抽象鳥類別,有許多抽象的方法(飛行、覓食 ... )留待子類實作。
使用上需要新增一個類別時,其內部就選擇每個抽象方法的實作方法。
需要維護修改時,就變更不同的實作方法即可。
回呼模式或者說是委派模式。
對介面編程並在適當的時候傳入實作(委派),執行期執行回呼實作(委派)的內容。
除非你的程式碼執行的比你想像中的要慢,否則別去優化。
假如你真的想優化,就必須先想好如何用資料證明,它的速度變快了。
畢竟不必要的改動也不一定對於結果有顯著的幫助,而且過早改動很容易跟其他原則有衝突。
Premature optimization is the root of all evil
― Donald Knuth
About Me
Jian-Min Huang
wide range skill set backend engineer
Research, Architecture, Coding, DB, Ops, Infra.
mainly write Java but also ❤️ Scala, Kotlin and Go